'''
Created on May 12, 2010

@author: roni
'''
from traceParser import TraceParser
import logging
import subprocess
import os
from projectConstants import ProjectConstants

class Ns2Scenario(object):
    '''
    Describes an NS2 experiment scenario
    '''
    INFINITY = 10**8
    
    # SCENARIO CONSTANST (IN FUTURE MAKE THIS PART OF CONFIG FILE)
    PACKET_SIZE = float(200*8)
    LINK_BANDWIDTH = float(10**6)
    LINK_DELAY = float(0.1)
    


    def __init__(self,config_file_name=None, simulate=False):
        '''
        Reads the scenario from the config file
        Parameters:
        config_file_name - The scenario configuration file, from which to load the scenario information
        simulate - If true, run an NS2 simulation on the scenario and load the resulting paths and expecetd_delays to self.
        Will not work if no configuration file was given.
        '''
        if config_file_name is not None:            
            self.load(config_file_name)
            if simulate:
                self.simulate()
        else:
            self.num_of_nodes = 0
            self.nodes = set()
            self.node_delays = dict()
            self.adjecancy_matrix = dict()
            self.edge_delays = dict()
            self.agent_to_flow = dict()
            self.agents = set()


    def add_node(self):
        self.num_of_nodes = self.num_of_nodes+1
        new_node=self.num_of_nodes-1
        self.nodes.add(new_node)    
        self.adjecancy_matrix[new_node,new_node]=0
        for i in range(0,self.num_of_nodes):
            self.adjecancy_matrix.setdefault((i,new_node),Ns2Scenario.INFINITY)
            self.adjecancy_matrix.setdefault((new_node,i),Ns2Scenario.INFINITY)        
        return new_node
    
    def add_edge(self,from_node, to_node, weight=LINK_DELAY):           
        self.adjecancy_matrix[from_node,to_node]=weight
    
    def add_agent(self,source,target):
        new_agent = len(self.agents)
        self.agents.add(new_agent)
        self.agent_to_flow[new_agent] = (source,target)
        return new_agent
    
    def remove_agent(self,agent):
        self.agents.remove(agent)
        self.agent_to_flow.pop(agent)
    
    def set_edge_delay(self,from_node, to_node,weight):
        self.edge_delays[from_node,to_node]=weight
    def set_node_delay(self,node,delay):
        self.node_delays[node] = delay

    def save(self,config_file_name):
        config_file = file(config_file_name,'w')
        config_file.write("nodes\n")
        config_file.write("%d\n" % self.num_of_nodes)
        delayed_nodes = self.node_delays.keys()
        if len(delayed_nodes)>0:
            config_file.write("delays\n")
            for node in delayed_nodes:
                config_file.write("%s,%d\n" % (node, self.node_delays[node]))
                
        config_file.write("edges\n")        
        for i in range(0,self.num_of_nodes-1):
            for j in range(i+1,self.num_of_nodes):
                if self.adjecancy_matrix[i,j]<Ns2Scenario.INFINITY and i!=j:
                    if self.edge_delays.has_key((i,j)):
                        config_file.write("%s,%s,%s\n" % (i,j,self.edge_delays[i,j]))
                    else:
                        config_file.write("%s,%s\n" % (i,j))
        
        config_file.write("generators\n")
        sorted_agents = list(self.agent_to_flow.keys())
        sorted_agents.sort()
        for agent in sorted_agents:
            config_file.write("%s,%s\n" % self.agent_to_flow[agent])        
        config_file.close()

    def load(self,config_file_name,do_path_calculations=False):        
        config_file = file(config_file_name,'r')
        
        # Read the number of nodes
        line = config_file.readline()
        if(line.strip().startswith("nodes")==False):
            raise ValueError("Illegal file format. Expected nodes")
        self.num_of_nodes = int(config_file.readline().strip())
        self.nodes = set(xrange(self.num_of_nodes))        
        line = config_file.readline()
        
        # Read the delays
        self.node_delays = dict()
        if(line.strip().startswith("delays")):
            line = self.read_delays(config_file)
        
        # Read the edges
        if(line.strip().startswith("edges")==False):
            raise ValueError("Illegal file format. Expected edges")        
        line = self.read_edges(config_file)
 
        # Read the agents (generators)
        if(line.strip().startswith("generators")==False):
            raise ValueError("Illegal file format. Expected generators")
        self.agent_to_flow = dict()
        agent_id = -1
        for line in config_file.readlines():
            parts = line.split(",")
            flow_source = int(parts[0].strip())
            flow_destination = int(parts[1].strip())
            agent_id = agent_id+1
            self.agent_to_flow[agent_id] = (flow_source, flow_destination)
        self.agents = set(xrange(len(self.agent_to_flow.keys())))
        
        # Calcualte routing path
        self.agent_to_path = dict()
        self.expected_delay = dict()        
        if do_path_calculations:
            self.calculate_paths()
    
    def simulate(self):
        ''' Run the NS2 simulator on the given scenario, and parse trace for delays'''
        trace_file_name = "Ns2Scenario.trace.temp"
        scenario_file_name = "Ns2Scenario.scenario.temp"
        scenario_file_full_path = "%s/%s" % (ProjectConstants.RESROUCE_DIR, scenario_file_name)
        
        # Save scenario to a file for it to be reached by NS2
        self.save(scenario_file_full_path)                 
        
        # Run NS2                
        os.chdir("../../ns2/")
        trace_file_full_path = "%s/%s" % (ProjectConstants.NS2_RESROUCE_DIR,trace_file_name)
        scenario_file_full_path = "%s/%s" % (ProjectConstants.NS2_RESROUCE_DIR, scenario_file_name)
        logging.info("Running simulation...")        
        value = subprocess.check_call(["ns", "runSimulation.tcl", \
                                       scenario_file_full_path,trace_file_full_path])
        os.chdir("../python/src/")        
        logging.info("Simulation done. Exit code: %s" % value)
        
        # Parse results
        trace_parser = TraceParser()
        trace_file_full_path = "%s/%s" % (ProjectConstants.RESROUCE_DIR,trace_file_name)
        
        # Update paths
        agent_to_details = trace_parser.parse(trace_file_full_path)        
        agent_to_paths = dict()
        for agent in agent_to_details:
            agent_to_paths[agent]=agent_to_details[agent].path
        self.calculate_expected_delays(agent_to_paths)

        return agent_to_details
    
    def calculate_expected_delays(self,agent_to_path):
        ''' Loads the agent's path into the internal datastructures self.agent_to_path and self.expected_delay
            Param: agent_to_path - dictionary of agent to a list of nodes that are the path from source to target 
            (excluding the source and target node)  '''
        self.agent_to_path = dict()
        self.expected_delay = dict()
        for agent in agent_to_path:
            path = agent_to_path[agent]
            expected_delay = 0
            current = self.agent_to_flow[agent][0]
            for node in path[1:]:
                expected_delay = expected_delay+self.calculate_total_link_delay(current,node)
                current=node
            
            self.agent_to_path[agent] = path
            self.expected_delay[agent] = expected_delay
    
    def calculate_total_link_delay(self,from_node, to_node):
        ''' Calculate the expected delay for a link '''
        delay = Ns2Scenario.PACKET_SIZE/Ns2Scenario.LINK_BANDWIDTH+Ns2Scenario.LINK_DELAY
        if self.edge_delays.has_key((from_node,to_node)):
            delay = delay+self.edge_delays[from_node, to_node]
        return delay
        
    def calculate_delay_in_abnormal_links(self,delay):
        ''' Return the number of links that need to be abnormal to explain the delay '''
        delay_per_link = Ns2Scenario.PACKET_SIZE/Ns2Scenario.LINK_BANDWIDTH+Ns2Scenario.LINK_DELAY
        return delay/delay_per_link        
    
    def read_edges(self,config_file):
        ''' Reads the edges from the config file. Also calculate edge weights 
        Returns the line after the last edge (for future use) '''            
        line = config_file.readline()
        
        # Initialize adjacency matrix
        self.adjecancy_matrix = dict()
        for i in range(0,self.num_of_nodes):
            self.adjecancy_matrix[i,i] = 0
            for j in range(0,self.num_of_nodes):
                self.adjecancy_matrix.setdefault((i,j),Ns2Scenario.INFINITY)       

        # Read edges,calculate expected delays
        self.edge_delays = dict()
        while (line.strip().startswith("generators")==False):
            parts = line.split(",")
            from_node = int(parts[0].strip())
            to_node = int(parts[1].strip())
            # If contains link level delay
            if (len(parts)>2): 
                self.edge_delays[from_node,to_node] = int(parts[2].strip())

            self.add_edge(from_node, to_node)
            self.add_edge(to_node, from_node)
            line = config_file.readline()
        return line        
                            
    def read_delays(self,config_file):
        ''' Reads the delay from the config file. Returns the line after the last delay (for future use) '''
        line = config_file.readline()

        while (line.strip().startswith("edges")==False):
            parts = line.split(",")
            node = int(parts[0].strip())
            delay = int(parts[1].strip())
            self.node_delays[node]=delay
            line = config_file.readline()
        return line
    
    def __eq__(self,obj):
        if self.num_of_nodes!=obj.num_of_nodes:
            return False
        if self.node_delays!=obj.node_delays:
            return False;
        if self.edge_delays!=obj.edge_delays:
            return False
        if self.agent_to_flow!=obj.agent_to_flow:
            return False
        if self.adjecancy_matrix!=obj.adjecancy_matrix:
            return False
        return True
